在學習 FP 的過程中,會看到 FP 常常被拿來跟 OOP 做比較,那 OOP 究竟是什麼呢?它們是對立關係只能選一種用嗎?誰比較有優勢呢?
今天會先簡單介紹 OOP 大概是什麼,以及幾個支持 OOP 的重要支柱們。
中文翻作「物件導向程式設計」,簡單來說就是以 物件(object) 為主體的程式設計風格。
這句話是否似曾相識呢?
沒錯,因為前三天提到的 FP(Functional Programming),就是以 函式(function) 為主體的程式設計風格。
基於讀者可能已經看完前面三天,對於 FP 這種程式設計風格有基礎概念了,所以我想讓讀者在開始學習 OOP 之前,有正確的學習觀念,以免被一些不由自主形成的對立影響了。
FP 是一種程式設計風格,OOP 則是另一種程式設計風格。
就好像 RPG 遊戲裡面玩騎士的角色擅長近距離戰鬥,但肯定也有不擅長的吧!所以我們會有其它角色像是弓箭手,擅長遠距離戰鬥。
FP 跟 OOP 就像是上述的關係,FP 擅長處理「流程」相關的問題,那遇到「非流程」相關的問題該怎麼辦?就可以考慮用其它像是 OOP 這種,擅長解決不同問題的程式設計風格。
何不全都要?
RPG 遊戲為了角色平衡,一次就只能選一種角色,但我們人類的潛力是無窮的,你可以是騎士在最前方衝鋒陷陣,也可以同時配一把弓,在適合遠距時攻擊。
文藝復興時期的達文西,在繪畫、音樂、建築、數學、幾何學、解剖學、生理學、動物學、植物學、天文學、氣象學、地質學、地理學、物理學、光學、力學、發明、土木工程等領域都有顯著的成就。被稱為「博學者」(polymath)。
有人會擔心,是不是整個團隊都一定要用 OOP 或都用 FP,不然會不會寫到一半風格有落差開始打架?
其實不會的,因為一個網頁包含了許多功能,每一個功能都是要解決使用者的問題,可以根據每個問題的特性,去思考要使用 OOP 或 FP,甚至其它,這樣才不會發生讓弓箭手近距離跟狂戰士互 A 的慘況。
結論是通通學起來!
既然已經確定 OOP 就是可以成為自己的助力,那就開始來學習吧!
OOP 是以 object 為主體的思考,所以我們要先來學習,Javascript 跟物件相關的兩個重點:Closures、Prototypal Inheritance。
中文翻作「閉包」,它是 JavaScript 的一個資訊隱藏機制。
先從我們常見的物件當範例吧:
const person = {
name: 'Joey',
age: 20
};
有夠普通的物件,但如果今天我們是個員工管理系統,可能需要紀錄員工的薪水:
const person = {
name: 'Joey',
age: 20,
salary: 40000,
};
阿捏母湯啊,我只要不小心印出 person.salary
就看光光了,甚至不小心 person.salary *= 3
就晉升中產階級了。
即便不是這麼刻意,也很容易因為 Object.entries(person)
這種不經意的列舉,就把比較隱私的資料暴露出來。
這邊所謂的「隱私」資料其實不一定是「不能被看到」,因為要是不能看乾脆就不要放就好啦!這個「隱私」比較是「避免無意中被修改」。
為了避免無意中被修改,我們需要讓某些 property 是 private,但 Javascript 並沒有提供這樣的關鍵字,以往都只能用 coding 的慣例來約束這件事,比如在私有特性的名稱前後綴一個底線(_),比如 _salary
,告訴其他開發者,這是一個不能直接被檢視、編輯的屬性。
但很明顯這種方式,防君子不防豬隊友,如果要硬改絕對是可以,因此它來了 - Closures。
要使用 closures 的機制,必須透過 function 來產生 object,先把上面的範例改成用 function 來產生:
const createPerson = (name, age) => {
const salary = 40000;
const getSalary = () => salary;
return {
name,
age,
getSalary
};
}
const person = createPerson('Joey', 20);
console.log(person.salary);
console.log(person.getSalary());
執行結果
undefined
40000
有發現這裡發生很神奇的事情嗎?乍看之下 salary
這個變數只有在 createPerson
函式的 scope 裡面,而且又沒有被回傳,感覺在函式外應該無法取得才對。。。
在函式中可以定義另一個函式時,如果內部的函式參照了外部的函式的變數,一旦外部的函式被執行,則產生閉包。
以上面的範例來說,會做以下判斷:
createPerson
裡面的 salary
沒有被存取salary
有出現在 getSalary
裡面getSalary
被回傳到外面去了salary
放到封閉的變數環境內.getSalary
(),就會到變數環境裡面把 salary
抓出來總結一句話就會是:內部函式變數有參照外部變數,就會產生 closures
Closures 機制會把資料儲存在他們封閉起來的變數環境中,不提供對這些變數的直接存取。唯一的辦法是,要在函式內明確提供存取它的方式。
中文是「原型繼承」。這東西大家應該就比較有概念了,因為這東西可能從學習 Javascript 的第一天,就已經在用繼承的東西了。
比如說:
const arr = ['Jack', 'Allen', 'Alice'];
arr.forEach(item => console.log(item));
看起來很熟悉對吧!但你有想過嗎?我們使用點運算子(.
)都是用在物件取得 property 對吧(比如說 person.name
),那這明明是個陣列,為什麼可以用 arr.forEach
?
這邊有個驚人的事實要告訴你:
const arr = ['Jack', 'Allen', 'Alice'];
console.log(typeof arr);
執行結果
"object"
是的各位觀眾,陣列在 Javascript 裡面,被分類在 object 裡面。
其實前幾天在講陣列的時候就隱約有提過,陣列其實就是 key 比較特別的物件,因為陣列只能用數字當 key。
現在你知道有繼承的概念了,就不難猜到,陣列就是繼承物件產生的,所以陣列可以使用點運算子(.
),也就一點都不奇怪了。
即便知道陣列是其中一種物件,但我又沒有在這個物件宣告 forEach
這個 property,為什麼還是可以直接用?
那是因為雖然我們宣告陣列都偷懶使用中括號:
const arr = ['Jack', 'Allen', 'Alice'];
但其實它在背後是這樣跑的:
const arr = new Array('Jack', 'Allen', 'Alice');
各位,「繼承」的關鍵語法登場了你有看到嗎?
隆重介紹!「繼承」的關鍵語法就是 new
!
new
的功能是「產生物件」,但要產生什麼物件呢?要在 new
後面放一個 constructor(建構子),定義要產生什麼物件,可以想像建構子就是個工廠,專門量產物件用的,而建構子必須要是一個 function,我們拿上面 closures 提到的例子來改一下:
const Person = function (name, age) {
const salary = 40000;
this.name = name;
this.age = age;
this.getSalary = () => salary;
}
const person = new Person('Joey', 20);
console.log(person.salary);
console.log(person.getSalary());
執行結果
undefined
40000
注意幾個重點:
this
關鍵字代表「這個物件」從範例可以看到:
Person
Person
,產生了一個 Person
物件Person
物件放到 person
變數如果以上你都有看懂,那能否回答我,為什麼 person.getSalary()
可以執行呢?
思考的分界線
沒錯,因為我們在 Person
的這個建構子函式中,有宣告了 getSalary
這個 property,因此 person
才可以直接使用 person.getSalary()
。
同理可證,上面那個 arr.forEach
的問題,你了解為什麼 arr.forEach()
可以執行了嗎 : )?
把原型想像成有一條鏈子,鏈子上勾著一個個物件,而每個物件的原型都指向前面的物件(可以透過 .__proto__ 找到上層):
const arr = [];
console.log(arr);
console.log(arr.__proto__); // Array 的原型
console.log(arr.__proto__.__proto__); // Object 的原型
console.log(arr.__proto__.__proto__.__proto__); // null
可以看到 arr
的 property 是一層一層繼承下來的,所以它才會同時有 Array 跟 Object 的原生屬性。
換句話說,原本你可以直接用 arr.forEach
,但如果打斷原型鏈,就會突然沒了爸爸的庇護(?):
const arr = [];
console.log(arr.forEach);
arr.__proto__ = null;
console.log(arr.forEach);
執行結果
ƒ forEach() { [native code] }
undefined
所以沒事不要亂斷絕父子關係啊(?),會少了很多援軍啊!
今天開始了 OOP 之旅,但其實也是藉 OOP 的名義,來學習一些跟 object 相關的進階觀念,當然其實是滿複雜的,平常很多 method 我們用得很順,也不曾認真去思考它從何而來。
當然我們不是來學考古的,即便不知道 arr.forEach
怎麼來的又怎樣?但那是因為要進到 OOP 的領域,有一些先備知識需要先掌握,明天才會如魚得水!
真正要上場實戰之前,還是要先蹲一下馬步囉!
我來自於你
來自於天空
來自於宇宙
它是 Javscript 的一個資訊隱藏機制。
"JavaScript" :)
感謝幫忙勘誤!不過我現在才發現我全部都拼 Javascript,s 小寫XD" 等寫完再一次改好了